/* uMMC.h

  uMMC Interface Library

  A library to communicate with the uMMC Data Storage Module
  or uMP3 Playback Module - File Control interface - 
  from Rogue Robotics (http://www.roguerobotics.com/).
  Requires
  uMMC firmware > 102.01
  or uMP3 firmware > 111.01
  See http://www.roguerobotics.com/faq/update_firmware for updating firmware.

  Written by Brett Hagman

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

Version Modified By Date     Comments
------- ----------- -------- --------
0001    B Hagman    09/06/21 Initial coding

*************************************************/

#include <stdint.h>
#include <wiring.h>
#include "uMMC.h"

/*************************************************
* Private Constants
*************************************************/

#define UMMC_MIN_FW_VERSION_FOR_NEW_COMMANDS 10201
#define UMP3_MIN_FW_VERSION_FOR_NEW_COMMANDS 11101

#define UMMC_STATS_SIZE                 0
#define UMMC_STATS_POSITION             1
#define UMMC_STATS_REMAINING            2

/*************************************************
* Constructor
*************************************************/

/*
uMMC::uMMC(int8_t (*_af)(void), int (*_pf)(void), int (*_rf)(void), void (*_wf)(uint8_t))
: LastErrorCode(0),
  _promptchar(UMMC_DEFAULT_PROMPT),
  _fwversion(0)
{
  _availablef = _af;
  _peekf = _pf;
  _readf = _rf;
  _writef = _wf;
}
*/
uMMC::uMMC(SerialBase &comms, int8_t useuMP3Protocol)
: LastErrorCode(0),
  _promptchar(UMMC_DEFAULT_PROMPT),
  _fwversion(0),
  _useuMP3Protocol(useuMP3Protocol)
{
  _comms = &comms;
}

/*************************************************
* Public Methods
*************************************************/

int8_t uMMC::sync(void)
{
  // procedure:
  // 1. sync (send ESC, clear prompt)
  // 2a. get prompt ("s p")
  // 2b. get version ("v")
  // 2c. change settings as needed
  // 3. check status
  // 4. close files (if needed - E08, or other error, not needed)
  uint8_t r;

  // 0. empty any data in the serial buffer
  _flush();

  // 1. sync
  _comms->write(0x1b);                  // send ESC to clear buffer on uMMC
  _read_blocked();                      // consume prompt


  // 2a. get prompt
  print('S');
  if(_useuMP3Protocol) { print('T'); };
  print('P'); print('\r');  // get our prompt (if possible)
  while(!_comms->available());
  r = _comms->peek();
  if(r != 'E')
  {
    _promptchar = _getnumber(10);
    _read_blocked();                    // consume prompt
  }
  else
  {
    _promptchar = UMMC_DEFAULT_PROMPT;  // this is an older version, no prompt char setting
    _get_response();                    // get our error
  }

  // 2b. get version

  _get_version();

  // 2c. change settings as needed
  // OLD: write timeout setting = 10 ms timeout
  // NEW: listing style = old style (0)
  if(_fwversion < UMMC_MIN_FW_VERSION_FOR_NEW_COMMANDS)
  {
    // we need to set the write timeout to allow us to control when a line is finished
    // in writeln.
    changesetting('1', 1);    // 10 ms timeout
  }
  else
  {
    // we're using the new version
    if(getsetting('L') != 0)
    {
      changesetting('L', 0);
    }
  }
  
  // 3. check status

  if(_useuMP3Protocol) { print('F'); print('C'); };
  print('Z'); print('\r');              // Get status
  
  if(_get_response())
    return -1;
  else
  {
    // good
    _read_blocked();                    // consume prompt

    // 4. close all files
    closeall();                         // ensure all handles are closed

    return 0;
  }
}


int8_t uMMC::changesetting(char setting, uint8_t value)
{
  print('S');
  if(_useuMP3Protocol) { print('T'); };
  print(setting); print((long)value); print('\r');
  
  return _get_response();
}


int16_t uMMC::getsetting(char setting)
{
  uint8_t value;
  
  print('S');
  if(_useuMP3Protocol) { print('T'); };
  print(setting); print('\r');
  
  while(!_comms->available());
  if(_comms->peek() != 'E')
  {
    value = _getnumber(10);
    _read_blocked();                    // consume prompt
  }
  else
  {
    value = _get_response();            // get the error
  }
  
  return value;
}


int8_t uMMC::getfreehandle(void)
{
  uint8_t r;

  if(_useuMP3Protocol) { print('F'); print('C'); };
  print('F'); print('\r');
  
  r = _read_blocked();

  if(r != 'E')
    r -= '0';                           // got our handle
  else
  {
    LastErrorCode = _getnumber(16);
    if(LastErrorCode == ERROR_NO_FREE_FILES)
      r = 0;
    else
      r = -1;
  }

  _read_blocked();                      // consume prompt
  
  return r;
}

int8_t uMMC::open(const char *filename)
{
  // defaults to READ
  return open(filename, OPEN_READ);
}

int8_t uMMC::open(const char *filename, open_mode mode)
{
  int8_t fh = getfreehandle();
  
  if(fh > 0)
    return open(fh, filename, mode);
  else
    return fh;
}

int8_t uMMC::open(int8_t handle, const char *filename)
{
  // defaults to READ
  return open(handle, filename, OPEN_READ);
}

int8_t uMMC::open(int8_t handle, const char *filename, open_mode mode)
{
  int8_t resp;

  if(_useuMP3Protocol) { print('F'); print('C'); };
  print('O'); write('0'+handle); print(' ');

  switch(mode)
  {
    case OPEN_READ:
    case OPEN_RW:
      print('R');
      if(mode == OPEN_READ) break;
    case OPEN_WRITE:
      print('W');
      break;
    case OPEN_APPEND:
      print('A');
      break;      
  }

  print(' '); print(filename);
  print('\r');

  resp = _get_response();
  return resp < 0 ? resp : handle;
}


void uMMC::close(int8_t handle)
{
  if(_useuMP3Protocol) { print('F'); print('C'); };
  print('C'); write('0'+handle); print('\r');
  _read_blocked();
}


void uMMC::closeall(void)
{
  if(_fwversion >= UMMC_MIN_FW_VERSION_FOR_NEW_COMMANDS)
  {
    if(_useuMP3Protocol) { print('F'); print('C'); };
    print('C'); print('\r');
    _read_blocked();                    // consume prompt
  }
  else
  {
    for(uint8_t i=1; i<=4; i++)
    {
      if(_useuMP3Protocol) { print('F'); print('C'); };
      print('C'); write('0'+i); print('\r');
      _get_response();
    }
  }
}


int8_t uMMC::opendir(const char *dirname)
{
  int8_t resp = 0;

  if((!_useuMP3Protocol && _fwversion >= UMMC_MIN_FW_VERSION_FOR_NEW_COMMANDS) ||
     (_useuMP3Protocol && _fwversion >= UMP3_MIN_FW_VERSION_FOR_NEW_COMMANDS))
  {
    if(_useuMP3Protocol) { print('F'); print('C'); };
    print('L'); print('S'); print(' '); print(dirname); print('\r');
    resp = _get_response();
    _read_blocked();                // consume prompt
    return resp;
  }
  else
  {
    LastErrorCode = ERROR_NOT_SUPPORTED;
    return -1;
  }
}

int32_t uMMC::filecount(const char *filemask)
{
  return filecount(NULL, filemask);
}


int32_t uMMC::filecount(const char *dirname, const char *filemask)
{
  int32_t fcount = 0;
  
  if(dirname)
  {
    if(opendir(dirname))
    {
      // opendir error
      return -1;
    }
  }

  if((!_useuMP3Protocol && _fwversion >= UMMC_MIN_FW_VERSION_FOR_NEW_COMMANDS) ||
     (_useuMP3Protocol && _fwversion >= UMP3_MIN_FW_VERSION_FOR_NEW_COMMANDS))
  {
    if(_useuMP3Protocol) { print('F'); print('C'); };
    print('L'); print('C'); print(' '); print(filemask); print('\r');

    if(_get_response() == 0)
    {
      fcount = _getnumber(10);
      
      _read_blocked();                 // consume prompt
      
      return fcount;
    }
    else
    {
      // error occurred with "LC" command
      return -1;
    }
  }
  else
  {
    LastErrorCode = ERROR_NOT_SUPPORTED;
    return -1;
  }
}


int8_t uMMC::readdir(char *filename, const char *filemask)
{
  // retrieve the next file from the directory
  // return -1 for no more files (EOF)

  // currently using the original file listing style, i.e. D/fs filename

  char c;

  if((!_useuMP3Protocol && _fwversion >= UMMC_MIN_FW_VERSION_FOR_NEW_COMMANDS) ||
     (_useuMP3Protocol && _fwversion >= UMP3_MIN_FW_VERSION_FOR_NEW_COMMANDS))
  {
    if(_useuMP3Protocol) { print('F'); print('C'); };
    print('L'); print('I'); print(' '); print(filemask); print('\r');

    if(_get_response())
    {
      // had an error
      if(LastErrorCode == ERROR_EOF)
        return -1;
      else
        return -2;
    }
    else
    {
      // we have the file info next
      while(!_comms->available());

      if(_comms->peek() == 'D')
      {
        // we have a directory
        _comms->read();                     // consume 'D'
      }
      else
      {
        // it's a file, with a file size
        _getnumber(10);                     // discard (for now)
      }

      _read_blocked();                      // consume separator ' '

      // now get filename
      while((c = _read_blocked()) != '\r')
      {
        *filename++ = c;
      }
      *filename = 0;                        // terminate string
      
      _read_blocked();                      // consume prompt
      
      return 0;
    }
  }
  else
  {
    LastErrorCode = ERROR_NOT_SUPPORTED;
    return -1;
  }
}



// Read a single byte from the given handle.
int16_t uMMC::readbyte(int8_t handle)
{
  uint8_t ch = 0;

  if(_useuMP3Protocol) { print('F'); print('C'); };
  print('R'); write('0'+handle); print(' '); print((long)1); print('\r');

  // we will get either a space followed by 1 byte, or an error
  
  if(_get_response())
  {
    // had an error
    if(LastErrorCode == ERROR_EOF)
      return -1;
    else
      return -2;
  }
  else
  {
    // we have a single byte waiting
    ch = _read_blocked();
    _read_blocked();                   // consume prompt
    return ch;
  }
}


int16_t uMMC::read(int8_t handle, uint16_t count, char *buffer)
{
  // read up to count bytes into buffer
  uint32_t bytesremaining;
  uint16_t i;

  // check first how many bytes are remaining
  bytesremaining = _get_filestats(handle, UMMC_STATS_REMAINING);

  if(bytesremaining > 0)
  {
    if(count > bytesremaining)
      count = bytesremaining;
    if(_useuMP3Protocol) { print('F'); print('C'); };
    print('R'); write('0'+handle); print(' '); print(count); print('\r');
  }
  else
  {
    return 0;
  }

  // now read count bytes
  
  if(_get_response())
  {
    if(LastErrorCode == ERROR_EOF)
      return -1;
    else
      return -2;
  }

  for(i=0; i<count; i++)
    buffer[i] = _read_blocked();

  _read_blocked();                      // consume prompt
  
  return i;                             // return number of bytes read
}


int16_t uMMC::readln(int8_t handle, uint8_t maxlength, char *tostr)
{
  int8_t r, i;
  
  if((!_useuMP3Protocol && _fwversion < UMMC_MIN_FW_VERSION_FOR_NEW_COMMANDS) ||
     (_useuMP3Protocol && _fwversion < UMP3_MIN_FW_VERSION_FOR_NEW_COMMANDS))
  {
    return -1;
  }
  
  if(_useuMP3Protocol) { print('F'); print('C'); };
  print('R'); print('L');               // Read a line, maxlength chars, maximum
  write('0'+handle); print(' ');
  print(maxlength);
  print('\r');
  
  if(_get_response())
  {
    if(LastErrorCode == ERROR_EOF)
      // EOF
      return -1;
    else
      return -2;
  }
  
  // otherwise, read the data

  i = 0;
  r = _read_blocked();

  while(r != _promptchar)               // we could have a blank line
  {
    tostr[i++] = r;
    r = _read_blocked();
  }

  tostr[i] = 0;                         // terminate our string

  return i;
}


int8_t uMMC::writeln(int8_t handle, const char *data)
{
  writeln_prep(handle);

  while(*data)
  {
    write(*data++);
    if(*data == '\r') break;
  }

  return writeln_finish();
}


void uMMC::writeln_prep(int8_t handle)
{
  // be warned: if using this command with firmwares less than 102.xx/111.xx
  // you must IMMEDIATELY write data within 10ms or the write time-out will occur
  
  if(_useuMP3Protocol) { print('F'); print('C'); };
  print('W');
  
  if((!_useuMP3Protocol && _fwversion >= UMMC_MIN_FW_VERSION_FOR_NEW_COMMANDS) ||
     (_useuMP3Protocol && _fwversion >= UMP3_MIN_FW_VERSION_FOR_NEW_COMMANDS))
  {
    print('L'); write('0'+handle);
  }
  else
  {
    write('0'+handle); print(' '); print(512);
  }
  
   print('\r');
}

int8_t uMMC::writeln_finish(void)
{
  if((!_useuMP3Protocol && _fwversion >= UMMC_MIN_FW_VERSION_FOR_NEW_COMMANDS) ||
     (_useuMP3Protocol && _fwversion >= UMP3_MIN_FW_VERSION_FOR_NEW_COMMANDS))
  {
    print('\r');
  }
  else
  {
    // we wait for more than 10ms to terminate the Write command (write time-out)
    delay(11);
  }

  return _get_response();
}


int8_t uMMC::write(int8_t handle, uint16_t count, const char *data)
{
  if(_useuMP3Protocol) { print('F'); print('C'); };
  print('W'); write('0'+handle); print(' '); print(count); print('\r');

  while(count--)
    write(*data++);

  // after we are done, check the response
  if(_get_response())
    return -1;
  else
    return 0;
}




uint32_t uMMC::getfilesize(int8_t handle) // get using "I fh" command
{
  return _get_filestats(handle, UMMC_STATS_SIZE);
}

uint32_t uMMC::getfileposition(int8_t handle)
{
  return _get_filestats(handle, UMMC_STATS_POSITION);
}

int8_t uMMC::seek(int8_t handle, uint32_t newposition)
{
  if((!_useuMP3Protocol && _fwversion >= UMMC_MIN_FW_VERSION_FOR_NEW_COMMANDS) ||
     (_useuMP3Protocol && _fwversion >= UMP3_MIN_FW_VERSION_FOR_NEW_COMMANDS))
  {
    if(_useuMP3Protocol) { print('F'); print('C'); };
    // J fh position
    print('J'); write('0'+handle);
  }
  else
  {
    // we need to do an empty read to seek to our position
    // R fh 0 position
    if(_useuMP3Protocol) { print('F'); print('C'); };
    print('R'); write('0'+handle); print(' '); print('0');
  }
  
  // common portion
  print(' '); print(newposition); print('\r');  

  return _get_response();
}

int8_t uMMC::seektoend(int8_t handle)
{
  if((!_useuMP3Protocol && _fwversion >= UMMC_MIN_FW_VERSION_FOR_NEW_COMMANDS) ||
     (_useuMP3Protocol && _fwversion >= UMP3_MIN_FW_VERSION_FOR_NEW_COMMANDS))
  {
    if(_useuMP3Protocol) { print('F'); print('C'); };
    // J fh E
    print('J'); write('0'+handle); print('E'); print('\r');
  }
  else
  {
    // two step process - get filesize, seek to that position
    return seek(handle, getfilesize(handle));
  }

  return _get_response();
}



/*************************************************
* Public (virtual)
*************************************************/

void uMMC::write(uint8_t c)
{
  _comms->write(c);
} 


/*************************************************
* Private Methods
*************************************************/

void uMMC::_flush(void)
{
  while(_comms->available())
    _comms->read();
}


int8_t uMMC::_read_blocked(void)
{
  // int8_t r;
  
  while(!_comms->available());
  // while((r = this->_readf()) < 0);   // this would be faster if we could guarantee that the _readf() function
                                        // would return -1 if there was no byte read
  return _comms->read();
}


int8_t uMMC::_get_response(void)
{
  // looking for a response
  // If we get a space " ", we return as good and the remaining data can be retrieved
  // " ", ">", "Exx>" types only
  uint8_t r;
  uint8_t resp = 0;

  // we will return 0 if all is good, error code otherwise

  r = _read_blocked();

  if(r == ' ' || r == _promptchar)
    resp = 0;

  else if(r == 'E')
  {
    LastErrorCode = _getnumber(16);     // get our error code
    _read_blocked();                    // consume prompt
    
    resp = -1;
  }
  
  else
  {
    LastErrorCode = 0xFF;               // something got messed up, a resync would be nice
    resp = -1;
  }
  
  return resp;
}


int16_t uMMC::_get_version(void)
{
  // return the version of the installed uMMC/uMP3
  print('V'); print('\r');
  
  // get first portion mmm.nn
  _fwversion = _getnumber(10);
  _read_blocked();                      // consume '.'
  _fwversion *= 100;
  _fwversion += _getnumber(10);
  
  while(_read_blocked() != _promptchar);  // consume up to and including prompt
  
  return _fwversion;
}


uint32_t uMMC::_get_filestats(int8_t handle, uint8_t valuetoget)
{
  uint32_t value = 0;

  if(_useuMP3Protocol) { print('F'); print('C'); };
  print('I'); write('0'+handle); print('\r');
  
  while(!_comms->available());
  if(_comms->peek() != 'E')
  {
    value = _getnumber(10);             // current file position (use for either position, or remaining)

    _read_blocked();                    // consume '/' or ' '

    if(valuetoget == UMMC_STATS_SIZE)
      value = _getnumber(10);           // file size
    else if(valuetoget == UMMC_STATS_REMAINING)
      value = _getnumber(10) - value;   // calculate bytes remaining (value assigned above)
    else
      _getnumber(10);                   // just waste the value

    _read_blocked();                    // consume prompt
  }
  // else return 0, error code is actually in .LastErrorCode
  
  return value;
}


int32_t uMMC::_getnumber(uint8_t base)
{
	uint8_t c, neg = 0;
	uint32_t val;

	val = 0;
	while(!_comms->available());
  c = _comms->peek();
  
  if(c == '-')
  {
    neg = 1;
    _comms->read();  // remove
    while(!_comms->available());
    c = _comms->peek();
  }
  
	while(((c >= 'A') && (c <= 'Z'))
	    || ((c >= 'a') && (c <= 'z'))
	    || ((c >= '0') && (c <= '9')))
	{
		if(c >= 'a') c -= 0x57;             // c = c - 'a' + 0x0a, c = c - ('a' - 0x0a)
		else if(c >= 'A') c -= 0x37;        // c = c - 'A' + 0x0A
		else c -= '0';
		if(c >= base) break;

		val *= base;
		val += c;
		_comms->read();                       // take the byte from the queue
		while(!_comms->available());          // wait for the next byte
		c = _comms->peek();
	}
	return neg ? -val : val;
}
